<?php

/**
 * Archivo que describe la clase User
 * @author Alejandro Gama <alejandro.gama@gmail.com>
 * @version 1.0
 * @package core
 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
 */
/**
 * Clase User
 * @author Alejandro Gama <alejandro.gama@gmail.com>
 * @version 1.0
 * @package core
 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
 */
namespace org\copperforest\oak;

class User
{

    /**
     * Usuario actualmente logueado
     * @access private
     * @var User
     */
    private static $current;

    /**
     * Array con los usuarios actualmente logueados
     * @access private
     * @var array
     */
    private static $usersOnline = array( );

    /**
     * Numero de usuarios logueados
     * @access private
     * @var int
     */
    private static $numOnline;
    private static $_registeredRoles;
    private static $_basicRoles;
    protected static $_cachedRolesGroup = array( ); //Array indexado por grupo que almacena los roles que tiene cada grupo que se solicita
    static private $_resetRoles = false;
    static private $_inmutable = false;

    /**
     * Identificador del usuario
     * @access private
     * @var int
     */
    private $id;
    private $roles = array( );
    private $ip;
    private $token;
    private $fingerPrint;
    private $where = '';
    private $group = 0;
    private $availablesGroups = array( );

    //   private $oldGroup;





    private function User( $id, $where = '' )
    {
        session_destroy();
        unset( $_SESSION );
        session_start();

        $this->id = $id;
        $this->where = $where;
        $this->ip = self::getRealIP();
        $this->fingerPrint = md5( FINGERPRINT . $_SERVER[ 'HTTP_USER_AGENT' ] );
        $this->token = md5( uniqid( mt_rand(), true ) );

        //ahora tengo que extraer los DAGs de roles para este usuario
        $basicos = MyLink::$default->get_rows( 'SELECT Rol, RolId FROM security_roles WHERE User=' . $this->id,
                        true )->toArray();

        $solicitados = array( );

        foreach ( $basicos as $b ) {
            self::_getInheritRoles( $b[ 0 ], $b[ 1 ], $this->roles );
            if ( !isset( $solicitados[ $b[ 0 ] ] ) )
                    $solicitados[ $b[ 0 ] ] = array( );

            $solicitados[ $b[ 0 ] ][ ] = $b[ 1 ];
        }

        // var_dump($this->roles);

        $this->group = self::_getCurrentGroup( $solicitados, $this ); //... el problema es que en estos momentos no hay $current
//var_dump($this->group);

        $primero = MyLink::$default->get_rows( 'SELECT first FROM security_users_accesses WHERE user=' . $this->id . ' ORDER BY first DESC LIMIT 1',
                        true )->getField( 0 );

        try {
            if ( !empty( $primero ) ) {
                $ultimo = MyLink::$default->get_rows( 'SELECT last FROM security_users WHERE user=' . $this->id . ' ORDER BY last DESC LIMIT 1',
                                true )->getField( 0 );

                $sql = 'UPDATE security_users_accesses SET last=\'' . $ultimo . '\' WHERE first=\'' . $primero . '\' AND user=' . $this->id;
                MyLink::$default->execute( $sql );
            } else {
                $sql = 'INSERT INTO security_users (user) VALUES (' . $this->id . ')';
                MyLink::$default->execute( $sql );
            }

            $sql = 'INSERT INTO security_users_accesses (user,ip,sid,last,suspect) VALUES (' . $this->id . ',' . MyLink::quote( $this->ip ) . ',' . MyLink::quote( session_id() ) . ',NOW(),\'N\')';
            MyLink::$default->execute( $sql );
        } catch ( Exception $e ) {
            var_dump( $e );
            session_destroy();
        }


        /*
          self::$usersOnline=MyLink::$default->get_rows('SELECT DISTINCT user, ip, sid FROM security_users_accesses WHERE last>(NOW()- INTERVAL 10 SECOND) AND user<>'.$id.' AND ip<>'.MyLink::quote($_SESSION['ip']).' AND sid<>'.MyLink::quote($SID), true)->toArray();
          self::$usersOnline[]=array($id,$_SESSION['ip'],$SID);
          self::$numOnline=count(self::$usersOnline);
         */

        //aqui se deería llamar a la funcion que definamos para almacenar en SESSION datos del usuario

        if ( function_exists( 'user_login_callback' ) && is_callable( 'user_login_callback' ) )
                call_user_func( 'user_login_callback', array( $this->id ) );
    }

    final static function _getInheritRoles( $rol, $rolId, &$apuntador )
    {
        if ( !isset( $apuntador[ $rol ] ) ) $apuntador[ $rol ] = array( );

        $apuntador[ $rol ][ $rolId ] = null;

        $heredados = MyLink::$default->get_rows( 'SELECT VerticeFin,VerticeFinId FROM security_edges WHERE contexto=\'rol\' AND Implicita=\'N\' AND VerticeInicio=' . MyLink::quote( $rol ) . ' AND VerticeInicioId=' . Mylink::quote( $rolId ),
                        true )->toArray();

        if ( !empty( $heredados ) ) {
            $apuntador[ $rol ][ $rolId ] = array( );

            foreach ( $heredados as $h )
                    self::_getInheritRoles( $h[ 0 ], $h[ 1 ],
                        $apuntador[ $rol ][ $rolId ] );
        }
    }

    final function useRoles( $roles = array( ) )
    {
        if ( self::$_inmutable )
                throw new Exception( "No se pueden cambiar de roles después del procesamiento del evento" ); //si lo permitieramos tendriamos que alterar self::$_useds

        $oldgroup = $this->group;
        $this->group = User::_getCurrentGroup( $roles );

        $this->resetRoles = (!empty( $oldgroup ) && $oldgroup != $this->group);
        return true;
    }

    final static function _registerRoles( $roles, $searched, $save = false )
    {
        foreach ( $roles as $tag => $ids ) {

            foreach ( $ids as $i => $heredados ) {

                if ( isset( $searched[ $tag ][ $i ] ) ) {

                    if ( !$save ) {
                        if ( !isset( self::$_basicRoles[ $tag ] ) )
                                self::$_basicRoles[ $tag ] = array( );

                        self::$_basicRoles[ $tag ][ ] = $i;
                    }

                    if ( !isset( self::$_registeredRoles[ $tag ] ) )
                            self::$_registeredRoles[ $tag ] = array( );
                    self::$_registeredRoles[ $tag ][ ] = $i;

                    if ( !empty( $heredados ) )
                            self::_registerRoles( $heredados, $searched, true );
                }
                else {
                    if ( $save ) {
                        if ( !isset( self::$_registeredRoles[ $tag ] ) )
                                self::$_registeredRoles[ $tag ] = array( );

                        self::$_registeredRoles[ $tag ][ ] = $i;
                    }

                    if ( !empty( $heredados ) )
                            self::_registerRoles( $heredados, $searched, $save );
                }
            }
        }
    }

    final static function _getCurrentGroup( $roles_solicitados = array( ),
            $user = null )
    {
        self::$_registeredRoles = array( );
        self::$_basicRoles = array( );

        if ( $user == null ) $user = self::$current;
        //self::_registerRoles(User::getLogged()->getRoles(), $roles_solicitados); //para esto OriginalRoles debería mantenerlo en sesion
        self::_registerRoles( $user->roles, $roles_solicitados ); //para esto OriginalRoles debería mantenerlo en sesion
        // var_dump(self::$_basicRoles);
        ksort( self::$_basicRoles );
        $rolString = '';

//var_dump(self::$_basicRoles);
//
        //Busco un grupo con estos roles básicos
        foreach ( self::$_basicRoles as $tag => &$ids ) {
            sort( $ids );

            foreach ( $ids as $i ) $rolString.='|' . $tag . '#' . $i;

            $rolString.='|';
        }

        $md5 = md5( $rolString );

        $opciones = MyLink::$default->get_rows( 'SELECT Id, RolStr FROM security_cache_groups WHERE md5=\'' . $md5 . '\'',
                        true )->toArray();

        foreach ( $opciones as $o ) {
            list($id_grupo, $str) = $o;

            if ( $str === $rolString ) {

                //guardo en cache los roles que tiene
                self::$_cachedRolesGroup[ ( int ) $id_grupo ] = self::$_registeredRoles;
                $user->availablesGroups[ ( int ) $id_grupo ] = true;

                return ( int ) $id_grupo;
            }
        }

        //no hay creado ningun grupo para el conjunto de roles
        ksort( self::$_registeredRoles );
        foreach ( self::$_registeredRoles as &$r ) sort( $r );

        $fullRolString = '';

        foreach ( self::$_registeredRoles as $tag => &$ids ) {
            sort( $ids );

            foreach ( $ids as $i ) $fullRolString.='|' . $tag . '#' . $i;

            $fullRolString.='|';
        }

        $sql = 'INSERT INTO security_cache_groups (md5,RolStr,FullRolStr) VALUES (\'' . $md5 . '\',' . MyLink::quote( $rolString ) . ',' . MyLink::quote( $fullRolString ) . ')';
        MyLink::$default->execute( $sql );

        self::$_cachedRolesGroup[ ( int ) MyLink::$default->insert_id ] = self::$_registeredRoles;
        $this->availablesGroups[ ( int ) MyLink::$default->insert_id ] = true;

        return ( int ) MyLink::$default->insert_id;
    }

    final static function _getRolesGroup( $grupo )
    {
        if ( isset( self::$_cachedRolesGroup[ ( int ) $grupo ] ) )
                return self::$_cachedRolesGroup[ ( int ) $grupo ];

        $roles = array( );

        $fullrolstr = MyLink::$default->get_rows( 'SELECT FullRolStr FROM security_cache_groups WHERE Id=' . ( int ) $grupo,
                        true )->getField( 0 );

        if ( !empty( $fullrolstr ) ) {

            $rls = explode( '|', substr( $fullrolstr, 1, -1 ) );

            foreach ( $rls as $r ) {

                $r = explode( '#', $r );

                if ( !isset( $roles[ $r[ 0 ] ] ) ) $roles[ $r[ 0 ] ] = array( );

                $roles[ $r[ 0 ] ][ ] = $r[ 1 ];
            }
        }

        self::$_cachedRolesGroup[ ( int ) $grupo ] = $roles;
        return $roles;
    }

    final function getId()
    {
        return $this->id;
    }

    final function getRoles()
    {
        return $this->roles;
    }

    final static function _wasReset()
    {
        self::$_inmutable = true;
        return self::$_resetRoles;
    }

    final function _getGroup()
    {
        return $this->group;
    }

    final function _isGroupAvailable( $group )
    {
        if ( isset( $this->availablesGroups[ ( int ) $group ] ) ) return true;

        //si no esta en availables...que en general debería estar..pero si entramos con una url directamente
        $rolstring = MyLink::$default->get_rows( 'SELECT RolStr FROM security_cache_groups WHERE Id=' . (( int ) $group),
                        true )->getField( 0 );

        if ( !empty( $rolstring ) ) {
            $rls = explode( '|', substr( $rolstring, 1, -1 ) );

            return self::_checkBranch( $this->roles, $rls );
        }

        return true;
    }

    final static function _checkBranch( $raiz, $pendientes )
    {
        foreach ( $raiz as $tag => $ids ) {

            foreach ( $ids as $i => $heredados ) {

                if ( in_array( $tag . '#' . $i, $pendientes ) )
                        $pendientes = array_diff( $pendientes,
                            array( $tag . '#' . $i ) );

                if ( empty( $pendientes ) || self::_checkBranch( $heredados,
                                $pendientes ) ) return true;
            }
        }

        return false;
    }

    function regenerateRoles()
    {
        $this->useRoles();
    }

    final function _validate()
    {
        if ( $this->ip == self::getRealIP() &&
                md5( FINGERPRINT . $_SERVER[ 'HTTP_USER_AGENT' ] ) == $this->fingerPrint &&
                ( $_SERVER[ 'REQUEST_METHOD' ] != 'POST' || $this->token == $_POST[ '_tkn' ] ) ) {

            //OJO SI CAMBIO EL TOKEN EN CADA LLAMADA VOY A TENER PROBLEMAS CON LAS LLAMADAS AJAX QUE HAGA EN UN FORMULARIO POST
            //Puedo cambiarlo cuando recibo un formulario y es correcto lo cambio para el siguiente
            //Si hago esto debería actualizar el token en todos los formularios despues de una llamada AJAX POST
            //if( $_SERVER['REQUEST_METHOD'] == 'POST' )
            // $this->token = md5(uniqid(mt_rand(), true));

            return true;
        }

        return false;
    }

    final static function _getToken()
    {
        return self::$current->token;
    }

    final static function getLogged()
    {
        return self::$current;
    }

    static function getNumOnline()
    {
        return self::$numOnline;
    }

    static function getOnline()
    {
        return self::$usersOnline;
    }

    final static function authenticate( $where )
    {
        //primero, si el where coincide con el del usuario actual no cambio nada

        if ( self::$current->where != $where ) {
            $id = MyLink::$default->get_rows( 'SELECT id FROM users WHERE ' . $where . ' LIMIT 1',
                            true )->getField( 0 );

            self::$current = new User( $id, $where );
        }

        $_SESSION[ 'User::current' ] = self::$current;

        //  var_dump(self::$current);
    }

    final static function _init()
    {
        //el cliente envia un identificador de session que debería comprobar que le pertenece,

        if ( isset( $_SESSION[ 'User::current' ] ) && $_SESSION[ 'User::current' ]->_validate() )
                self::$current = $_SESSION[ 'User::current' ];
        else self::$current = new User( 0 );

        //esto puede provocar que en un cierre inesperado de la aplicacion nos quedemos sin usuario en sesion
        //unset($_SESSION['User::current']);
        //de hecho puedo hacer aqui ya la asignacion... aunque si luego hacen un authenticate debería cambiarlo
        $_SESSION[ 'User::current' ] = self::$current;
        // var_dump(self::$current);
    }

    final static function _finish()
    {
        //ojo, hay que ejecutarlo antes de que se cierre la conexion
        //puedo eliminar filas de la tabla de ids de sesion para sesiones antiguas actualizando el last en security_users_accesses

        if ( self::$current->getId() != 0 ) {

            $sql = 'UPDATE security_users SET last=NOW(),`group`=' . self::$current->group . ' WHERE  user=' . self::$current->id;
            MyLink::$default->execute( $sql );

            //   $_SESSION['User::current']=self::$current;
        }
    }

    final static function getRealIP()
    {

        if ( isset( $_SERVER[ 'HTTP_X_FORWARDED_FOR' ] ) && $_SERVER[ 'HTTP_X_FORWARDED_FOR' ] != '' ) {
            $client_ip =
                    (!empty( $_SERVER[ 'REMOTE_ADDR' ] ) ) ?
                    $_SERVER[ 'REMOTE_ADDR' ] :
                    ( (!empty( $_ENV[ 'REMOTE_ADDR' ] ) ) ?
                            $_ENV[ 'REMOTE_ADDR' ] :
                            "unknown" );

            // los proxys van añadiendo al final de esta cabecera
            // las direcciones ip que van "ocultando". Para localizar la ip real
            // del usuario se comienza a mirar por el principio hasta encontrar
            // una dirección ip que no sea del rango privado. En caso de no
            // encontrarse ninguna se toma como valor el REMOTE_ADDR

            $entries = split( '[, ]', $_SERVER[ 'HTTP_X_FORWARDED_FOR' ] );

            reset( $entries );
            while ( list(, $entry) = each( $entries ) ) {
                $entry = trim( $entry );
                if ( preg_match( "/^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/", $entry,
                                $ip_list ) ) {
                    $private_ip = array(
                        '/^0\./',
                        '/^127\.0\.0\.1/',
                        '/^192\.168\..*/',
                        '/^172\.((1[6-9])|(2[0-9])|(3[0-1]))\..*/',
                        '/^10\..*/' );

                    $found_ip = preg_replace( $private_ip, $client_ip,
                            $ip_list[ 1 ] );

                    if ( $client_ip != $found_ip ) {
                        $client_ip = $found_ip;
                        break;
                    }
                }
            }
        } else {
            $client_ip =
                    (!empty( $_SERVER[ 'REMOTE_ADDR' ] ) ) ?
                    $_SERVER[ 'REMOTE_ADDR' ] :
                    ( (!empty( $_ENV[ 'REMOTE_ADDR' ] ) ) ?
                            $_ENV[ 'REMOTE_ADDR' ] :
                            "unknown" );
        }

        return $client_ip;
    }

}

//register_shutdown_function(array('User','_finish'));
//puedo registrar un metodo shutdown en el que haga la actualizacion del usuario
?>